package edu.gatech.fiveminutesleft;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Locale;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.location.Address;
import android.location.Geocoder;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.OverlayItem;

import edu.gatech.fiveminutesleft.model.LocationName;

/**
 * An activity which provides a simple panning view of google maps.
 * 
 * @author daniel keyes
 */
public class LocationViewActivity extends MapActivity {

	private MapView					mapView;
	private EditText				addressField;
	private Button					searchButton;

	private LocationName			location;

	private List<Overlay>			mapOverlays;
	private Drawable				drawable;
	private LocationItemizedOverlay	itemizedOverlay;

	/**
	 * Launches this activity
	 * 
	 * @param source
	 *            Activity that launched this
	 * @param location
	 *            a reference to the location data associated with a task
	 * @param editable
	 *            true to allow the user to input new locations, false to disable these functions
	 */
	public static void launch(Activity source, LocationName location, boolean editable) {
		Intent n = new Intent(source, LocationViewActivity.class);
		GUISpace.put(LocationViewActivity.class, "editable", editable);
		GUISpace.put(LocationViewActivity.class, "location", location);
		GUISpace.put(LocationViewActivity.class, "address", location.getAddress());
		source.startActivity(n);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.google.android.maps.MapActivity#onCreate(android.os.Bundle)
	 */
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		setContentView(R.layout.location_view_layout);

		mapView = (MapView) findViewById(R.id.mapview);
		mapView.setBuiltInZoomControls(true); // allow user to zoom in/out

		addressField = (EditText) findViewById(R.id.locationAddress);
		searchButton = (Button) findViewById(R.id.locationSearchButton);

		mapOverlays = mapView.getOverlays();
		// TODO: get a better pin image
		drawable = this.getResources().getDrawable(R.drawable.pin);
		itemizedOverlay = new LocationItemizedOverlay(drawable);

		if (!GUISpace.getBoolean(getClass(), "editable")) {
			addressField.setEnabled(false);
			searchButton.setEnabled(false);
		}
		location = (LocationName) GUISpace.get(getClass(), "location");
		addressField.setText(GUISpace.getString(getClass(), "address"));

		String addressText = addressField.getText().toString();
		if (addressText.length() > 0) {

			GeoPoint p = createGeoPoint(addressText);

			if (p != null) {
				zoomToGeopoint(p);
			} else {
				// print that address was improperly formatted
				mapView.getController().setZoom(1);
				Toast.makeText(this, "Location not found by google maps", Toast.LENGTH_SHORT).show();
				Log.d("LocationView", "address not found");
			}
		}

	}

	/**
	 * onClick method for the locationSearchButton "Go!".
	 * 
	 * @param view
	 *            Android-passed variable for the button that was clicked
	 */
	public void setLocation(View view) {

		// this works on version 4.0.3, but not 2.3.3 of the emulator. It should function on actual
		// phones.

		// we could use Geocoder.isPresent() to determine whether the phone supports Geocoder
		// functionality, but isPresent() doesn't always reflect the actual state of the phone.

		String addressText = addressField.getText().toString();
		if (addressText.length() > 0) {

			GeoPoint p = createGeoPoint(addressText);

			if (p != null) {
				zoomToGeopoint(p);
				// store new location in the task edit data
				location.setAddress(addressText);
				Toast.makeText(this, "Location set to " + location.getAddress(), Toast.LENGTH_SHORT).show();

			} else {
				// print that address was improperly formatted

				mapView.getController().setZoom(1);
				Toast.makeText(this, "Location not found by google maps", Toast.LENGTH_SHORT).show();
				Log.d("LocationView", "address not found");
			}
		}
	}

	/**
	 * Helper method to create a GeoPoint based on an address. Due to some weirdness in
	 * implementation with geocoding, this attempts first to use the phone's built-in Geocoder, then
	 * if that fails, to use the Google Maps API.
	 * 
	 * @param addressText
	 *            A string to search for, such as an address or location name
	 * @return a GeoPoint or null if no value was found
	 */
	private GeoPoint createGeoPoint(String addressText) {
		// use android's geocoder implementation to find a location
		Geocoder geocoder = new Geocoder(this, Locale.getDefault());
		GeoPoint p = null;
		try {
			List<Address> addresses = geocoder.getFromLocationName(addressText, 5);
			if (addresses.size() > 0) {
				// use the first address as the location
				p = new GeoPoint((int) (addresses.get(0).getLatitude() * 1E6), (int) (addresses.get(0).getLongitude() * 1E6));
			}
		} catch (IOException e) {

			// Geocoder will thrown an IOException if it's not supported on the platform
			// Really, platforms should define the Geocoder.isPresent() method, but adherence to
			// this is flaky.

			Log.w("LocationView", "Geocoder failed with message: " + e.getMessage());
			Log.w("LocationView", "Attempting Google Maps API workaround.");

			try {
				p = getGeoPoint(getLocationInfo(addressText));
			} catch (ClientProtocolException e1) {
				Log.w("LocationView", "Maps API failed with message: " + e1.getMessage());
			} catch (JSONException e1) {
				Log.w("LocationView", "Maps API failed with message: " + e1.getMessage());
			} catch (IOException e1) {
				Log.w("LocationView", "Maps API failed with message: " + e1.getMessage());
			}
		}

		return p;

	}

	/**
	 * Sets the map's view to the particular geopoint and zooms to that location
	 * 
	 * @param p
	 *            the GeoPoint to zoom to
	 */
	private void zoomToGeopoint(GeoPoint p) {
		mapView.getController().animateTo(p);
		mapView.getController().setZoom(12);

		OverlayItem overlayitem = new OverlayItem(p, "", "");
		itemizedOverlay.setOverlay(overlayitem);
		mapOverlays.add(itemizedOverlay);
	}

	/**
	 * Zooms out
	 */
	private void zoomOut() {
		mapView.getController().zoomOut();
	}

	/**
	 * A workaround for Geocoding presented by pablolar...@gmail.com on
	 * https://code.google.com/p/android/issues/detail?id=8816#c21 Uses google maps API as a way to
	 * map addresses to geopoints
	 * 
	 * @param address
	 *            A string to search for, such as an address or location name
	 * @return a JSONArray of matches for the address
	 * @throws IOException
	 *             if an error ocurred during any part of the http connection
	 * @throws ClientProtocolException
	 *             if an error occured while connecting to google maps
	 * @throws JSONException
	 *             if the fetched data could not be formatted as a JSON object
	 */
	public static JSONObject getLocationInfo(String address) throws JSONException, ClientProtocolException, IOException {

		// right now, we're uri-encoding spaces
		// we really should check all the characters which are changed by uri encoders
		HttpGet httpGet = new HttpGet("http://maps.google.com/maps/api/geocode/json?address="
				+ address.replaceAll(" ", "%20") + "ka&sensor=false");
		HttpClient client = new DefaultHttpClient();
		HttpResponse response;
		StringBuilder stringBuilder = new StringBuilder();

		response = client.execute(httpGet);
		HttpEntity entity = response.getEntity();
		InputStream stream = entity.getContent();
		int b;
		while ((b = stream.read()) != -1) {
			stringBuilder.append((char) b);
		}

		JSONObject jsonObject = new JSONObject();
		jsonObject = new JSONObject(stringBuilder.toString());

		return jsonObject;
	}

	/**
	 * A workaround for Geocoding presented by pablolar...@gmail.com on
	 * https://code.google.com/p/android/issues/detail?id=8816#c21 Uses google maps API as a way to
	 * map addresses to geopoints
	 * 
	 * @param jsonObjecta
	 *            JSONArray of locations address
	 * @return a GeoPoint corrosponding to the first address in the array or null if the array is
	 *         empty
	 * @throws JSONException
	 *             if the JSONObject is improperly formatted
	 */
	public static GeoPoint getGeoPoint(JSONObject jsonObject) throws JSONException {

		Double lon = new Double(0);
		Double lat = new Double(0);

		if (((JSONArray) jsonObject.get("results")).length() == 0) {
			return null;
		}

		lon = ((JSONArray) jsonObject.get("results")).getJSONObject(0).getJSONObject("geometry").getJSONObject("location")
				.getDouble("lng");

		lat = ((JSONArray) jsonObject.get("results")).getJSONObject(0).getJSONObject("geometry").getJSONObject("location")
				.getDouble("lat");

		return new GeoPoint((int) (lat * 1E6), (int) (lon * 1E6));
	}

	/**
	 * @param view
	 */
	public void confirm(View view) {
		String addressText = addressField.getText().toString();
		if (addressText.length() > 0) {
			GeoPoint p = createGeoPoint(addressText);
			if (p != null) {
				zoomToGeopoint(p);
				// store new location in the task edit data
				location.setAddress(addressText);
				if (GUISpace.getBoolean(getClass(), "editable"))
					Toast.makeText(this, "Location set to " + location.getAddress(), Toast.LENGTH_SHORT).show();
			} else {
				location.setAddress(addressText);
				if (GUISpace.getBoolean(getClass(), "editable"))
					Toast.makeText(this, "Location set to " + location.getAddress() + "(not on map)", Toast.LENGTH_SHORT)
							.show();
			}
		}
		finish();
	}

	@Override
	protected boolean isRouteDisplayed() {
		// required for subclasses of MapActivity
		// if we we want to get any route data, we have to agree to google's terms of use
		// which we do by returning true. go figure. -dk
		return false;
	}

	/**
	 * A simple ItemizedOverlay that stores one location. Admittedly, since it only stores one
	 * object, it doesn't make much sense to have an Itemized overlay.
	 * 
	 * @author daniel
	 */
	private class LocationItemizedOverlay extends ItemizedOverlay<OverlayItem> {
		private OverlayItem	location;

		public LocationItemizedOverlay(Drawable marker) {
			super(boundCenterBottom(marker));
		}

		public void setOverlay(OverlayItem overlay) {
			location = overlay;
			populate();
		}

		protected OverlayItem createItem(int i) {
			return location;
		}

		public int size() {
			return 1;
		}
	}

}
